Skip to content

bugfix: Save victory status to prevent early exits from resulting in defeat in network matches#2292

Open
Stubbjax wants to merge 9 commits intoTheSuperHackers:mainfrom
Stubbjax:fix-victory-conditions
Open

bugfix: Save victory status to prevent early exits from resulting in defeat in network matches#2292
Stubbjax wants to merge 9 commits intoTheSuperHackers:mainfrom
Stubbjax:fix-victory-conditions

Conversation

@Stubbjax
Copy link

@Stubbjax Stubbjax commented Feb 11, 2026

This change fixes an issue where a player's victory is not reliably recorded at the end of a network match.

If a player quits the game at any point before the score screen, and it results in the destruction of their assets (i.e. no allies exist), then a loss is recorded, regardless of the actual outcome. This is because the victory condition is processed at the time the game ends and the score screen is shown. To resolve this, the player's victory is now saved as soon as one team remains and the match is considered over, and so any subsequent defeat + asset destruction has no effect on the recorded outcome.

The change includes a minor refactor by abstracting some of the update logic into a multipleAlliancesExist function, and consolidating some stat conditions. There are further refactors that can be done in follow-up changes, such as reading m_isDefeated instead of repeatedly calling hasSinglePlayerBeenDefeated in multiple places, and splitting several blocks in the update method into functions.

Tests

Below is a series of network match test results to verify the fix.

Test 1

Knife surrenders at the start of a 1v1 match. Fork then quits the game.

Player Team Old Outcome New Outcome
Knife - Loss Loss
Fork - Loss Win*

Test 2

Knife surrenders at the start of a 1v1v1 match. Fork then surrenders. Spoon then quits the game.

Player Team Old Outcome New Outcome
Knife - Loss Loss
Fork - Loss Loss
Spoon - Loss Win*

Test 3

Knife surrenders at the start of a 2v1 match. Fork then surrenders. Spoon then quits the game.

Player Team Old Outcome New Outcome
Knife 1 Loss Win*
Fork - Loss Loss
Spoon 1 Loss Win*

Test 4

Fork surrenders at the start of a 2v1 match. Spoon then quits the game. Knife then quits the game.

Player Team Old Outcome New Outcome
Knife 1 Loss Win*
Fork - Loss Loss
Spoon 1 Win Win

Test 5

Knife surrenders at the start of a 2v1 match. Spoon then surrenders. Fork then quits the game.

Player Team Old Outcome New Outcome
Knife 1 Loss Loss
Fork - Loss Win*
Spoon 1 Loss Loss

@Stubbjax Stubbjax self-assigned this Feb 11, 2026
@Stubbjax Stubbjax added Bug Something is not working right, typically is user facing Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Refactor Edits the code with insignificant behavior changes, is never user facing labels Feb 11, 2026
@greptile-apps
Copy link

greptile-apps bot commented Feb 11, 2026

Greptile Summary

This PR fixes a bug where players who quit before the score screen would incorrectly receive a loss even if they won. The fix caches victory status in a new m_isVictorious array when the match ends (single alliance remaining), preventing asset destruction during quit from affecting the outcome.

Changes:

  • Added m_isVictorious[MAX_PLAYER_COUNT] array to track victory status
  • Refactored alliance checking into multipleAlliancesExist() function
  • Added markAllianceVictorious() to cache victory status when match ends, including defeated allies
  • Modified hasAchievedVictory() to check cached m_isVictorious instead of recalculating
  • Modified hasBeenDefeated() to check cached m_isDefeated instead of inferring from victory status
  • Refactored ScoreScreen.cpp to consolidate win/loss/disconnect stats tracking into single if-else chain

Issues:

  • Minor indentation inconsistency in GeneralsMD version (spaces vs tabs)
  • Edge case mentioned in previous review still exists but appears to be handled correctly (simultaneous defeat results in loss for all players)

Confidence Score: 3/5

  • Safe to merge with minor style fix needed for GeneralsMD indentation
  • Core logic fix is sound and addresses the reported bug correctly. Test results demonstrate the fix works as intended. ScoreScreen refactoring is clean and improves maintainability. The edge case concern from previous review (simultaneous defeat) appears to be handled correctly by the existing logic. Score reduced to 3 only due to indentation inconsistency and the conceptual ambiguity of multipleAlliancesExist() not distinguishing zero vs one alliance.
  • Fix indentation in GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp line 97 (spaces instead of tabs)

Important Files Changed

Filename Overview
Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp Added m_isVictorious array to cache victory status, refactored alliance checking; edge case bug remains where simultaneous elimination could leave players in inconsistent state
GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp Same changes as Generals version but with indentation inconsistency (spaces instead of tabs) on line 97; same edge case bug exists

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[VictoryConditions::update called] --> B{Is multiplayer?}
    B -->|No| Z[Return]
    B -->|Yes| C{Single alliance remaining?}
    C -->|Yes| G[Check for player eliminations]
    C -->|No| D[multipleAlliancesExist?]
    D -->|Yes: Multiple teams alive| Z
    D -->|No: Single team or none| E[Set m_singleAllianceRemaining = true<br/>Save m_endFrame]
    E --> F[findFirstUndefeatedPlayer]
    F -->|Player found| H[markAllianceVictorious<br/>Set m_isVictorious for alliance]
    F -->|nullptr: All defeated| G
    H --> G
    G --> I{For each player}
    I --> J{Player defeated?}
    J -->|Yes| K[Set m_isDefeated<br/>Reveal map<br/>Kill player]
    J -->|No| I
    K --> I
    I -->|Done| L[Check local player defeat]
    L --> Z
    
    style H fill:#90EE90
    style K fill:#FFB6C1
    style E fill:#FFD700
Loading

Last reviewed commit: db4f4c7

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 11, 2026

Additional Comments (2)

Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
logic issue: losing players who quit early without asset destruction won't be marked as defeated

The old logic returned true for any player when m_singleAllianceRemaining && !hasAchievedVictory(player). The new logic only checks m_isDefeated[i], which is only set when hasSinglePlayerBeenDefeated() returns true (assets destroyed). If a losing player quits early and their assets are maintained by allies, they won't be marked defeated.

Add fallback logic:

Bool VictoryConditions::hasBeenDefeated(Player *player)
{
	if (!player)
		return false;

	if (!m_singleAllianceRemaining)
		return false;

	for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
	{
		if (player == m_players[i])
		{
			if (m_isDefeated[i])
				return true;
			if (!m_isVictorious[i])
				return true;
			return false;
		}
	}

	return false;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
Line: 287:302

Comment:
logic issue: losing players who quit early without asset destruction won't be marked as defeated

The old logic returned true for any player when `m_singleAllianceRemaining && !hasAchievedVictory(player)`. The new logic only checks `m_isDefeated[i]`, which is only set when `hasSinglePlayerBeenDefeated()` returns true (assets destroyed). If a losing player quits early and their assets are maintained by allies, they won't be marked defeated.

Add fallback logic:

```cpp
Bool VictoryConditions::hasBeenDefeated(Player *player)
{
	if (!player)
		return false;

	if (!m_singleAllianceRemaining)
		return false;

	for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
	{
		if (player == m_players[i])
		{
			if (m_isDefeated[i])
				return true;
			if (!m_isVictorious[i])
				return true;
			return false;
		}
	}

	return false;
}
```

How can I resolve this? If you propose a fix, please make it concise.

GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
logic issue: losing players who quit early without asset destruction won't be marked as defeated

The old logic returned true for any player when m_singleAllianceRemaining && !hasAchievedVictory(player). The new logic only checks m_isDefeated[i], which is only set when hasSinglePlayerBeenDefeated() returns true (assets destroyed). If a losing player quits early and their assets are maintained by allies, they won't be marked defeated.

Add fallback logic:

Bool VictoryConditions::hasBeenDefeated(Player *player)
{
	if (!player)
		return false;

	if (!m_singleAllianceRemaining)
		return false;

	for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
	{
		if (player == m_players[i])
		{
			if (m_isDefeated[i])
				return true;
			if (!m_isVictorious[i])
				return true;
			return false;
		}
	}

	return false;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
Line: 289:304

Comment:
logic issue: losing players who quit early without asset destruction won't be marked as defeated

The old logic returned true for any player when `m_singleAllianceRemaining && !hasAchievedVictory(player)`. The new logic only checks `m_isDefeated[i]`, which is only set when `hasSinglePlayerBeenDefeated()` returns true (assets destroyed). If a losing player quits early and their assets are maintained by allies, they won't be marked defeated.

Add fallback logic:

```cpp
Bool VictoryConditions::hasBeenDefeated(Player *player)
{
	if (!player)
		return false;

	if (!m_singleAllianceRemaining)
		return false;

	for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
	{
		if (player == m_players[i])
		{
			if (m_isDefeated[i])
				return true;
			if (!m_isVictorious[i])
				return true;
			return false;
		}
	}

	return false;
}
```

How can I resolve this? If you propose a fix, please make it concise.

@Stubbjax Stubbjax force-pushed the fix-victory-conditions branch from e688101 to 87a75a4 Compare February 11, 2026 06:52
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 146 to 148
Bool VictoryConditions::multipleAlliancesExist()
{
Player* alive = nullptr;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect win detection
multipleAlliancesExist() returns false when zero non-defeated players exist (e.g., simultaneous elimination / edge cases where everyone is defeated). In that case update() will set m_singleAllianceRemaining = true and cache a “victorious” player based on the first non-defeated player found, but there won’t be one, so no one gets marked victorious. This makes hasAchievedVictory() always return false, and can also prevent hasBeenDefeated() from ever returning true (since it now only consults m_isDefeated[i], not “not victorious”), leaving the match outcome inconsistent.

This function needs to distinguish “exactly one alliance alive” from “nobody alive” (and handle the latter explicitly).

Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
Line: 146:148

Comment:
**Incorrect win detection**
`multipleAlliancesExist()` returns `false` when *zero* non-defeated players exist (e.g., simultaneous elimination / edge cases where everyone is defeated). In that case `update()` will set `m_singleAllianceRemaining = true` and cache a “victorious” player based on the first non-defeated player found, but there won’t be one, so no one gets marked victorious. This makes `hasAchievedVictory()` always return false, and can also prevent `hasBeenDefeated()` from ever returning true (since it now only consults `m_isDefeated[i]`, not “not victorious”), leaving the match outcome inconsistent.

This function needs to distinguish “exactly one alliance alive” from “nobody alive” (and handle the latter explicitly).

How can I resolve this? If you propose a fix, please make it concise.

void VictoryConditions::markAllianceVictorious(Player* victoriousPlayer)
{
// This marks the player and any allies as victorious, including defeated allies.
// This also ensures players retain their victorious status if their assets are destroyed after
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add the TheSuperHackers @bugfix here and remove the other @bugfix comment, then move this comment above the function to get it a bit out of the way.

}

//-------------------------------------------------------------------------------------------------
Player* VictoryConditions::findFirstVictoriousPlayer()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name is a bit misleading. It only returns the first victorious player if multipleAlliancesExist returns false. Otherwise it returns the first non defeated player.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a better name would be findFirstUndefeatedPlayer or findFirstAlivePlayer. The function does not consider multipleAlliancesExist.

}

//-------------------------------------------------------------------------------------------------
Player* VictoryConditions::findFirstVictoriousPlayer()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conceptually, bool multipleAlliancesExist + Player* findFirstNotDefeatedPlayer + markAllianceVictorious(Player*) is a bit cumbersome logic.

A better logic would be Player* getSingleAllianceSurvived + markAllianceVictorious(Player*)
.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking for a single surviving alliance does not conceptually work either - we still want the condition to be satisfied if there are no alliances remaining.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

virtual UnsignedInt getEndFrame( void ) { return m_endFrame; } ///< on which frame was the game effectively over?
private:
Player* findFirstUndefeatedPlayer(); ///< Find the first player that has not been defeated.
void markAllianceVictorious(Player* victoriousPlayer); ///< Mark the victorious player and his allies as victorious.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uses spaces instead of tabs for indentation

Suggested change
void markAllianceVictorious(Player* victoriousPlayer); ///< Mark the victorious player and his allies as victorious.
void markAllianceVictorious(Player* victoriousPlayer); ///< Mark the victorious player and his allies as victorious.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
Line: 97

Comment:
uses spaces instead of tabs for indentation

```suggestion
	void markAllianceVictorious(Player* victoriousPlayer); ///< Mark the victorious player and his allies as victorious.
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker Refactor Edits the code with insignificant behavior changes, is never user facing ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments